/*******************************************************************************
 * Copyright (c) 2011 Technische Universität Darmstadt
 * 					  Fachbereich Informatik
 * 					  Theoretische Informatik - Kryptographie und Computeralgebra
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 ******************************************************************************/

package de.tud.cdc;

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowEvent;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.EOFException;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.URLDecoder;
import java.security.InvalidKeyException;
import java.security.NoSuchProviderException;
import java.security.Security;
import java.sql.SQLException;
import java.util.Iterator;
import java.util.Stack;
import java.util.Vector;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;

import javax.swing.BoxLayout;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JPasswordField;
import javax.swing.border.EmptyBorder;
import javax.swing.text.BadLocationException;
import javax.swing.text.MutableAttributeSet;
import javax.swing.text.html.HTML;
import javax.swing.text.html.HTMLEditorKit.ParserCallback;
import javax.swing.text.html.parser.ParserDelegator;

import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.openpgp.PGPCompressedData;
import org.bouncycastle.openpgp.PGPEncryptedDataList;
import org.bouncycastle.openpgp.PGPException;
import org.bouncycastle.openpgp.PGPLiteralData;
import org.bouncycastle.openpgp.PGPObjectFactory;
import org.bouncycastle.openpgp.PGPOnePassSignatureList;
import org.bouncycastle.openpgp.PGPPrivateKey;
import org.bouncycastle.openpgp.PGPPublicKeyEncryptedData;
import org.bouncycastle.openpgp.PGPSecretKey;
import org.bouncycastle.openpgp.PGPSecretKeyRing;
import org.bouncycastle.openpgp.PGPSecretKeyRingCollection;
import org.bouncycastle.openpgp.PGPUtil;
import org.bouncycastle.util.encoders.Base64;

/**
 * CryptoBird. Core class for all cryptographic background functions (mainly
 * pgp) used in the CryptoBird extension.
 * 
 * @author Franziskus Kiefer
 * 
 */
public class CryptoBird {

	/**
	 * Special characters to fix encoding in some mails.
	 */
	private static String[] utf8Chars = {"Ãœ","Ã¼","Ã¶","Ã¤","Ã„","ÃŸ","Ã©","Ã¨","Ã³","Ã±","â€™","Ã£","Â´","Ãª´","Ã§´","Ã‡´","â€ž","â€"};
	/**
	 * SQL derby error code for wrong password.
	 */
	private static final String WRONG_DB_PWD = "XBM06";
	/**
	 * SQL derby error code for a too short password.
	 */
	//private static final String TOO_SHORT_DB_PWD = "XBM07";
	/**
	 * SQL derby error code for db in use.
	 */
	private static final String DB_IN_USE = "XSDB6";
	/**
	 * List with PGP passwords for decryption, saved in a {@link Vector} of {@link String}s. 
	 */
	private Vector<String> passwds = new Vector<String>(); 
	/**
	 * Name of the secret PGP key-ring.
	 */
	private String SECRET_KEYRING;
	/**
	 * {@link String} constant for not found keys.
	 */
	private static final String KEY_NOT_FOUND = "sk for message not found.";
	private static final String ERROR = "---ERROR---";
	/**
	 * {@link Logger} to print on stdout.
	 */
	private final Logger logger;
	
	
	/**
	 * {@link Database} to store the decrypted mails in. This has to be encrypted!
	 */
	private Database db;
	/**
	 * Buffer for a very long mail that arrives in multiple parts from the javascript part of this extension.
	 */
	private StringBuffer mailBuffer;
	/**
	 * According to the mail buffer above we also need a buffer for the id.
	 */
	private String idBuffer = new String();
	/**
	 * {@link String} containing the id's of an encrypted email, to display the user in password prompt.
	 */
	private String identities;
	
	//TESTING VARIABLE
	private int cnt = 0;


	public static void main(String[] args) {
		// nothing to do here, just for exporting...
	}
	
	public CryptoBird() {
		Security.addProvider(new BouncyCastleProvider());
		mailBuffer = new StringBuffer();
		logger = Logger.getLogger("CBLog");
		logger.setLevel(Level.INFO);
	    db = new Derby(logger);
	}
	
	public CryptoBird(boolean debug){
		this();
	}

	/**
	 * Set parameters for PGP encryption and database. And initialize the
	 * database.
	 * 
	 * @param pgp_path
	 *            the path to the pgp folder containing the secret key ring.
	 * @param gpg_secret_keyring
	 *            the name of the pgp secret key ring. Path to the pgp secret
	 *            keyring as {@link String}
	 * @param gpg_public_keyring
	 *            Path to the pgp public keyring as {@link String}
	 * @param password
	 *            the database password as {@link String}
	 * @param dbPath
	 */
	public String initialize(String pgp_path, String gpg_secret_keyring, String gpg_public_keyring, String password, String dbPath) {
		this.SECRET_KEYRING = pgp_path + File.separator + gpg_secret_keyring;
		File secring = new File(this.SECRET_KEYRING);
		String result = "";
		if (!secring.isFile()){
			result = "Can't find the GPG secret keyring. Please edit the path and restart if you use GPG encrypted emails.";
			this.SECRET_KEYRING = null;
		}
		synchronized (db) {
			try {
				db.init(dbPath, password);
			} catch (SQLException e) {
				SQLException cause = (SQLException) e.getCause();
				while (cause != null){
					if (cause.getSQLState().equals(WRONG_DB_PWD)) {
						result = "Wrong database password!";
						db = new Derby(logger);
					}
					else if (cause.getSQLState().equals(DB_IN_USE)){
						result = "Database is already in use. Please close all other connections and klick OK.";
						db = new Derby(logger);
					}
					if (cause.getCause() instanceof SQLException)
						cause = (SQLException) cause.getCause();
					else
						break;
				}
			} catch (InvalidKeyException e){
				if (e.getMessage().equals("no unlimited strength policy")){
					result = "Database could not be started. Java Unlimited Strength Policy seems to be not installed properly. ";
				} else {
					e.printStackTrace();
				}				
			}
		}
		return result;
	}
	
	/**
	 * Add a password to the pgp passwords
	 * 
	 * @param pw
	 *            the password as {@link String}
	 */
	public void addPassword(String pw) {
		this.passwds.add(pw);
	}
	
	/**
	 * remove a mail from the db
	 * 
	 * @param uri
	 *            the uri to remove as {@link String}
	 */
	public void removeMail(String uri) {
		synchronized (db) {
			db.deleteEntry(uri);
		}
	}

	/**
	 * remove a whole folder
	 * 
	 * @param uri
	 *            the base uri of the folder to delete as {@link String}
	 */
	public void removeFolder(String uri) {
		synchronized (db) {
			db.deleteEntries(uri);
		}
	}

	/**
	 * change the uri of a mail in the db
	 * 
	 * @param oldUri
	 *            the old uri as {@link String}
	 * @param newUri
	 *            the new uri as {@link String}
	 */
	public void changeId(String oldUri, String newUri) {
		synchronized (db) {
			db.changeEntry(oldUri, newUri);
		}
	}
	
	/**
	 * add a new uri to mail in the db
	 * XXX: so we have a copy in the db atm. should be more space saving
	 * 
	 * @param oldUri
	 *            the old uri as {@link String}
	 * @param newUri
	 *            the new uri as {@link String}
	 */
	public void addCopiedId(String oldUri, String newUri) {
		synchronized (db) {
			db.addCopiedEntry(oldUri, newUri);
		}
	}
	
	/**
	 * Change the base uri of several mails in the db (moved a folder)
	 * 
	 * @param oldUri
	 *            the old uri as {@link String}
	 * @param newUri
	 *            the new uri as {@link String}
	 */
	public void changeFolder(String oldUri, String newUri) {
		synchronized (db) {
			db.changeEntryLike(oldUri, newUri);
		}
	}
	
	/**
	 * Copy a folder, so copy those mails and change their base URI (copied a folder)
	 * 
	 * @param oldUri
	 *            the old URI as {@link String}
	 * @param newUri
	 *            the new URI as {@link String}
	 */
	public void copyFolder(String oldUri, String newUri) {
		synchronized (db) {
			db.copyEntryLike(oldUri, newUri);
		}
	}

	/**
	 * Add a SMIME email, so that the body parameter is a plain email. HTML is
	 * allready parsed by javascript (TB utility functions)
	 * 
	 * @param id
	 *            the uri of the mail as {@link String}
	 * @param body
	 *            the body of the mail as {@link String}
	 * @param indexing
	 *            XXX: remove (not necessary anymore)
	 * @param encoding
	 *            XXX: remove (not necessary anymore)
	 * @return XXX: remove (not necessary anymore)
	 */
	synchronized public boolean addSmimeMail(String id, String body, boolean indexing, String encoding, String header) {
		// TODO: check here if it is in the db...so we do not parse it
		logger.finest(header);		
		logger.info("cnt: " + (++cnt));
		logger.info("id: "+id);
		
		// try to get a correct encoding
		String parsed = getCorrectEncoding(body); // TODO: not nice and does not correct everything 
		logger.finest(parsed);
		
		if (!header.startsWith("-----SUBJECT ENCRYPTED-----") || header.length()<28) header="";
		synchronized (db) {
			db.addMail(id, parsed, header); 
		}
		System.gc();
		return true;
	}
	
	/**
	 * get all uris where the body of the according mail matches the toSearch
	 * {@link String}
	 * 
	 * @param toSearch
	 *            the {@link String} to search for
	 * @return an Array of {@link String}s containing the found uris
	 */
	public String[] getMailIdWhere(String toSearch, boolean body, boolean subject) {
		synchronized (db) {
			return db.getMailIdWhere(toSearch.trim(), body, subject);
		}
	}
	
	
	/**
	 * get all uris where the body of the according mail matches the toSearch
	 * {@link String} and the uri matches the folder {@link String}
	 * 
	 * @param toSearch
	 *            the {@link String} to search for
	 * @param folder
	 *            the folder as {@link String} (base uri) to search in
	 * @return an Array of {@link String}s containing the found uris
	 */
	public String[] getMailIdWhereFolder(String toSearch, String folder, boolean body, boolean subject) {
		synchronized (db) {
			return db.getMailIdWhereFolder(toSearch.trim(), folder, body, subject);
		}
	}
	
	/**
	 * reset the database.
	 */
	public void resetDatabase() {
		synchronized (db) {
			db.clear();
		}
	}

	/**
	 * call this if a pgp mail is too large to process at once. initializes a mail
	 * with the uri and the first part of the mail body
	 * 
	 * @param part
	 *            the first part of the mail body as {@link String}
	 * @param id
	 *            the uri, identifying the mail as {@link String}
	 */
	synchronized public void initMailParts(String part, String id) {
		if (this.SECRET_KEYRING != null){
			this.mailBuffer.append(part);
			this.idBuffer = id;
		}
	}

	/**
	 * add a part of a too large pgp mail.
	 * {@link #initMailParts(String, String)} has to be called first!
	 * 
	 * @param part
	 *            a first part of the mail body as {@link String}
	 * @param finish
	 *            boolean value, true if this is the last part of the mail, else
	 *            false
	 */
	synchronized public void addDecryptedMailPart(String part, boolean finish, String header) {
		if (this.SECRET_KEYRING != null){
			mailBuffer.append(part);
			if (finish && this.idBuffer != null){
				saveDecryptedMail(this.idBuffer, this.mailBuffer.toString(), false, null, header);
			}
		}
	}

	/**
	 * Save a pgp encrypted mail with a given URI (id).
	 * 
	 * @param id
	 *            the URI of the mail as {@link String}
	 * @param encryptedMail
	 *            the mail body to decrypt as {@link String}
	 * @param indexing
	 *            XXX: not necessary anymore
	 * @param encoding
	 *            XXX: not necessary anymore
	 * @return XXX:not necessary anymore
	 */
	synchronized public boolean saveDecryptedMail(String id, String encryptedMail, boolean indexing, String encoding, String header) {
		if (this.SECRET_KEYRING != null){
			logger.info("cnt: " + (++cnt));
			logger.info("id: "+id);
			// need threading here to force the calling javascript to wait for this
			SaveMailThread save = new SaveMailThread(id, encryptedMail, encoding, header);
			save.start();
			// wait for thread until it saved the mail
			synchronized (save) {
				try {
					save.wait();
				} catch (InterruptedException e) {
					e.printStackTrace();
					logger.warning("Error while waiting for password prompt.\n"+e.getMessage());
				}
			}
			return save.abort;
		} else {
			return true;
		}
	}

	/**
	 * SaveMailThread class. Used to save pgp encrypted mails into the db (
	 * {@link Database}). Extends {@link Thread}.
	 * 
	 * @author Franziskus Kiefer
	 * 
	 */
	private class SaveMailThread extends Thread {
		
		private String id;
		private String encryptedMail;
		private String encoding;
		private boolean abort = false;
		private String header;

		/**
		 * Constructor for {@link SaveMailThread}. Initialized with a mail URI
		 * and mail body.
		 * 
		 * @param id
		 *            the URI of the mail that should be saved as {@link String}
		 * @param mail
		 *            the mail body of the mail to save as {@link String}
		 * @param encoding
		 *            XXX: not used anymore
		 */
		public SaveMailThread(String id, String mail, String encoding, String header) {
			this.id = id;
			this.encryptedMail = mail;
			this.encoding = encoding;
			this.header = header;
		}
		
		/**
		 * Start the encryption of the mail. Save it into the db {@link Database} afterwards.
		 */
		@Override
		public void run() {
			boolean checked = false;
			// check at first if the id is not in the DB allready
			synchronized (db) {
				checked  = !db.checkID(id);
			}
			if (checked){
				logger.finest("Id: "+id+" will be stored to DB...");
				// save new incoming mail
				abort = parseAndSave(encryptedMail, id, encoding, header);
			}
			logger.finest("Id: "+id+" was NOT be stored to DB. It is already in.");
			id = null;
			encryptedMail = null;
			synchronized (this) {
				notify();
			}
		}
	}

	/**
	 * Get the charset of a multipart encrypted mail (pgp)
	 * 
	 * @param mail
	 *            the mail as {@link String} containing a "charset=???" part,
	 *            hopefully
	 * @return a {@link String} with the charset or NULL
	 */
	private String getCharset(String mail){
		String charset = null;
		if (mail.contains("Charset:")){
			charset = mail.substring(mail.indexOf("Charset: ") + 9, mail.indexOf(System.getProperty("line.separator"), mail.indexOf("Charset: ")));
			logger.finest("encoding (getCharset (:)): "+charset);
		} else if (mail.indexOf("charset=") != -1) {
			charset = mail.substring(mail.indexOf("charset=") + 8, mail.indexOf(System.getProperty("line.separator"), mail.indexOf("charset=")));
			logger.finest("encoding (getCharset (=)): "+charset);
		} else if (mail.contains("charset")){ // for HTML charset with %3D
			charset = mail.substring(mail.indexOf("charset%3D") + 9, mail.indexOf(System.getProperty("line.separator"), mail.indexOf("charset%3D")));
			logger.finest("encoding (getCharset (:)): "+charset);
		}
		if (charset != null){
			charset = charset.replaceAll("\r\n", "").replaceAll("\n", "").replaceAll(System.getProperty("line.separator"),"").replaceAll("\r", "");
			charset = charset.replaceAll("\"", "").trim();
		}
		return charset;
	}

	/**
	 * Core function to handle pgp encrypted mails.
	 * 
	 * @param encryptedMail
	 *            the encrypted mail as {@link String}
	 * @param id
	 *            the URI of the given mail as {@link String}
	 * @param encoding
	 *            mail encoding, got from TB
	 * @return XXX: not used anymore (perhaps helpful for TB asking for pwd?!)
	 */
	private boolean parseAndSave(String encryptedMail, String id, String encoding, String header) {
		if (encryptedMail == null) { // is not happening anymore i hope (Bug: too long messages)
			logger.warning("Nullpointer Exception on beginning of parseAndSave. encryptedMail is NULL!");
			return true; // -> remove
		} else {
			String charset = null;
			if (encoding != null && encoding.length() > 3)
				charset = encoding;
			else if (encryptedMail.contains("Charset: ")){ 			// get encoding
				charset = getCharset(encryptedMail);
			}
			try {
				String mail = KEY_NOT_FOUND;
				// need to get the ids from the key to display anyway...so call decrypt without pwd
				if (this.passwds.isEmpty()){
					mail = parsePGPMail(encryptedMail, id, charset, mail, -1, header);
				} else {
					for (int i = 0; i < passwds.size(); i++) {
						mail = parsePGPMail(encryptedMail, id, charset, mail, i, header);
						if (mail != KEY_NOT_FOUND)
							break;
					}
				}
				if (mail.equals(KEY_NOT_FOUND)) { 
					// id format: imap-message://alice@imap.cdc.informatik.tu-darmstadt.de/Sent#7

					// askForPWD(folder);
					if (this.identities != null){
						// no key found -> ask for password, but only if no exception was thrown!
						PwdPrompt pwdPrompt = new PwdPrompt(this.identities);
						pwdPrompt.pack();
						pwdPrompt.setLocationRelativeTo(null);
						pwdPrompt.setVisible(true);
						synchronized (pwdPrompt) {
							pwdPrompt.wait();
						}
						// check if pwd prompt was closed (OK was not pressed) -> abort
						if (pwdPrompt.abort){
							return false;
						}
						return parseAndSave(encryptedMail, id, encoding, header);
					} else {
						logger.warning("Can't find the secret key in the keyring. This mail won't be searchable!");
						return true; // the secret key is not in the keyring!
					}
				}
			} catch (Exception e) {
				e.printStackTrace();
			}
		}
		return true; // something went wrong.... but go on
	}

	private String parsePGPMail(String encryptedMail, String id, String charset, String mail, int i, String header) {
		ByteArrayInputStream in = new ByteArrayInputStream(encryptedMail.getBytes());
		String pwd = "";
		if (i != -1)
			pwd = passwds.get(i);
		try {
			if (!encryptedMail.isEmpty())
				mail = decryptMail(in, pwd.toCharArray(), charset);
			else
				mail = "";
		} catch (Exception e) {
			logger.finest("Error in parseAndSave" + e.getLocalizedMessage());
			logger.warning("Mail URI was:\n"+id);
			return KEY_NOT_FOUND;
		}
		// check if multipart and parse it
		if (mail.contains("ontent-Type: multipart/mixed;") || mail.contains("ontent-Type: multipart/signed")){
			mail = parseMultipart(mail);
		} else if (mail.contains("ontent-Type: text/plain") || mail.contains("ontent-Type: text/html")){
			mail = parseDecryptedMail(mail);
		}
		logger.finest(mail);
		String decSub = decryptSubject(header, pwd.toCharArray());
		logger.finest(decSub);
		if (!mail.equals(KEY_NOT_FOUND) && !decSub.equals(KEY_NOT_FOUND)) { // save decrypted mail in db
			synchronized (db) {
				db.addMail(id, mail, decSub);
			}
			return mail;
		}
		return KEY_NOT_FOUND;
	}

	/**
	 * Parse a decrypted mail and check the encoding if it is a multipart email.
	 * 
	 * @param mail
	 *            the decrypted pgp-mail as {@link String}
	 * @return the parsed, decrypted mail as {@link String}
	 */
	private String parseDecryptedMail(String mail) {
		String sh = mail.replaceAll("Content-(Type|ID|Disposition|Transfer-Encoding):.*", "");
		sh = sh.replaceAll("--", "");
		sh = sh.replaceAll("=([A-Fa-f0-9]{2})", "%$1"); // XXX: what if there is = in the mail?! // XXX: is this correct for every encoding?
		sh = sh.replaceAll("%([A-Fa-f0-9][^A-Fa-f0-9])", "%25$1"); // escape all regular % XXX: this can be made nicer
		sh = sh.replaceAll("%([^A-Fa-f0-9][A-Fa-f0-9])", "%25$1"); // escape all regular %
		sh = sh.replaceAll("%([^A-Fa-f0-9][^A-Fa-f0-9])", "%25$1"); // escape all regular %
		boolean html = sh.contains("<html>");
		String result;
		try {
			result = URLDecoder.decode(sh, "ISO-8859-1"); //FIXME: need correct encoding String here! ISO-8859-1
		} catch (Exception e) {
			logger.warning("Exception while decoding multipart mail (parseDecryptedMail).\n"+e.getMessage());
			// so use undecoded to save at least something
			result = sh;
		}
		result.trim();
		String encoding = "ISO-8859-1";
		if (result.toLowerCase().contains("charset"))
			encoding = getCharset(result);
		if (encoding == null)
			encoding = "ISO-8859-1";
		if (html)
			result = parseHTML(result, encoding);
		return result;
	}

	/**
	 * Parse an html {@link String} to get its content.
	 * 
	 * @param s
	 *            the HTML {@link String} to parse
	 * @param encoding
	 *            the encoding (not used atm!)
	 * @return the content of the HTML {@link String}
	 */
	private String parseHTML(String s, String encoding) {
		ParserDelegator delegator = new ParserDelegator();
		Parser p = new Parser();
		// the third parameter is TRUE to ignore charset directive
		try {
			delegator.parse(new StringReader(s), p, true);
		} catch (IOException e) {
			e.printStackTrace();
			return "";
		}
		return p.getText(encoding);
	}

	/**
	 * Parser class, extending {@link ParserCallback} to parse an HTML
	 * {@link String}.
	 * 
	 * @author Franziskus Kiefer
	 * 
	 */
	private class Parser extends ParserCallback {
		/**
		 * {@link StringBuffer} for the output String.
		 */
		StringBuffer s = new StringBuffer();

		/**
		 * Get the parse content of the HTML {@link String}
		 * 
		 * @param encoding
		 *            not used atm.
		 * @return the content as {@link String}
		 */
		public String getText(String encoding) {
			return getCorrectEncoding(s.toString());
		}

		Stack<HTML.Tag> stack = new Stack<HTML.Tag>();

		@Override
		public void flush() throws BadLocationException {
		}
		
		@Override
		public void handleComment(char[] data, int pos) {
		}

		@Override
		public void handleStartTag(HTML.Tag tag, MutableAttributeSet a, int pos) {
			// get a tag and push it into a stack
			stack.push(tag);
		}
		@Override
		public void handleEndTag(HTML.Tag t, int pos) {
			// this would be correct. but seems that some files are no
			// correct html
		}
		@Override
		public void handleSimpleTag(HTML.Tag t, MutableAttributeSet a, int pos) {
		}
		
		@Override
		public void handleError(String errorMsg, int pos) {
		}
		
		@Override
		public void handleEndOfLineString(String eol) {
		}

		@Override
		public void handleText(char[] data, int pos) {
			if (!stack.empty()) {
				HTML.Tag tag = stack.peek();
				s.append(data);
				if (tag.breaksFlow())
					s.append("\n");
			}
		}

	}

	/**
	 * Utility function to make a {@link String} out of byte array.
	 * 
	 * @param b
	 *            the byte array
	 * @return the byte array as {@link String}
	 * @throws Exception
	 *             throws an exception if there is something wrong with the byte
	 *             array
	 */
	public static String getHexString(byte[] b) throws Exception {
		String result = "";
		for (int i = 0; i < b.length; i++) {
			result += Integer.toString((b[i] & 0xff) + 0x100, 16).substring(1);
		}
		return result;
	}

	/**
	 * Core function for pgp-encryption using bouncy castle.
	 * 
	 * @param in
	 *            the pgp encrypted mail as {@link InputStream}
	 * @param passwd
	 *            password for the private pgp key in the keyring as char array
	 * @param charset
	 *            the charset of this mail. If unknown this could be null.
	 * @return the decrypted mail as {@link String}
	 * @throws IOException
	 * @throws NoSuchProviderException
	 * @throws Exception
	 */
	@SuppressWarnings("rawtypes")
	public String decryptMail(InputStream in, char[] passwd, String charset) throws IOException, NoSuchProviderException {
		in = PGPUtil.getDecoderStream(in);
		StringBuffer error = new StringBuffer();
		try {
			FileInputStream keyInStream = null;
			try {
				keyInStream  = new FileInputStream(SECRET_KEYRING);
			} catch (FileNotFoundException e) {
				e.printStackTrace();
				return e.toString();
			}
			
			PGPEncryptedDataList enc;
			PGPObjectFactory pgpF = new PGPObjectFactory(in);
			Object o = pgpF.nextObject();
			//
			// the first object might be a PGP marker packet.
			//
			if (o instanceof PGPEncryptedDataList) {
				enc = (PGPEncryptedDataList) o;
			} else {
				enc = (PGPEncryptedDataList) pgpF.nextObject();
			}
			
			//
			// find the secret key
			//
			Iterator it = enc.getEncryptedDataObjects();
			PGPPrivateKey sKey = null;
			PGPPublicKeyEncryptedData pbe = null;
			PGPSecretKeyRingCollection pgpSec = new PGPSecretKeyRingCollection(PGPUtil.getDecoderStream(keyInStream));
			keyInStream.close();
			while (sKey == null && it.hasNext()) {
				pbe = (PGPPublicKeyEncryptedData) it.next();
				sKey = findSecretKey(pgpSec, pbe.getKeyID(), passwd);
			}
			if (sKey == null) {
				return KEY_NOT_FOUND;
			}
			
			InputStream clear = pbe.getDataStream(sKey, "BC");
			PGPObjectFactory plainFact = new PGPObjectFactory(clear);
			Object message = plainFact.nextObject();
//			clear.close();
			PGPObjectFactory pgpFact = null;
			if (message instanceof PGPCompressedData) {
				PGPCompressedData cData = (PGPCompressedData) message;
				 pgpFact = new PGPObjectFactory(cData.getDataStream());

				message = pgpFact.nextObject();
			}
			ByteArrayOutputStream bOut = new ByteArrayOutputStream();
			if (message instanceof PGPLiteralData) {
				PGPLiteralData ld = (PGPLiteralData) message;

				InputStream unc = ld.getInputStream();
				int ch;
				try {
					while ((ch = unc.read()) >= 0) {
						bOut.write(ch);
					}
				} catch (Exception e) {
					// something went wrong here in BC...but message should be decrypted! (Few (first or) last characters could be missing)
					// so, do nothing here...
					// Should not happen anymore!
					e.printStackTrace();
				}
//				unc.close();
			} else if (message instanceof PGPOnePassSignatureList) {
				// write message out
				PGPLiteralData ld = (PGPLiteralData) pgpFact.nextObject();

				InputStream unc = ld.getInputStream();
				int ch;

				while ((ch = unc.read()) >= 0) {
					bOut.write(ch);
				}
				unc.close();

			} else {
				// write message out
				PGPLiteralData ld = (PGPLiteralData) message;

				InputStream unc;
				try{
					unc = ld.getInputStream();
				} catch (Exception e) {
					// why is here a Nullpointer thrown?
					logger.finest("Exception while getting inputstream from BC (decryptMail).\n"+e.getMessage());
					NullPointerException ex = new NullPointerException();
					ex.setStackTrace(e.getStackTrace());
					throw ex;
				}
				int ch;

				while ((ch = unc.read()) >= 0) {
					bOut.write(ch);
				}
				unc.close();
			}

			if (pbe.isIntegrityProtected()) {
				if (!pbe.verify()) {
					logger.warning("message failed integrity check");
				} else {
					logger.finest("message integrity check passed");
				}
			} else {
				logger.finest("no message integrity check");
			}
			
			/**
			 * Try to get correct encoding here.
			 * Does not work every time!
			 */
			if (charset == null || charset.equals("")){
				logger.warning("No Charset for this mail. So use ISO-8859-1. May lead to incorrect encoding.");
				charset = "ISO-8859-1";
//				charset = "UTF-8";
			}
			byte[] ba = bOut.toByteArray();
			String decrypted = null;
			try{
				decrypted = new String(ba, charset);
				if (!decrypted.toLowerCase().contains("charset") && decrypted.matches("<U+[0-9A-Fa-f]{4}>")){
					
				}
				decrypted = decrypted.replaceAll("=([A-Fa-f0-9]{2})", "%$1");
			} catch (Exception e) {
				logger.severe("Wrong encoding: "+charset + " " +e.getLocalizedMessage());
				String resultString = new String(ba);
				decrypted = resultString;
			}
			return decrypted; // TODO: Encoding: need to check if UTF-8... 
		} catch (PGPException e) {
			if (e.toString().equals("org.bouncycastle.openpgp.PGPException: checksum mismatch at 0 of 20"))
				return KEY_NOT_FOUND;
			logger.severe("Error in decrypt mail:\n"+e);
			error.append(e.toString());
			if (e.getUnderlyingException() != null) {
				e.getUnderlyingException().printStackTrace();
				error.append(e.getUnderlyingException().toString());
			}
		} catch (EOFException e) {
			// TODO: Need to fix this!
			logger.warning("Error in BouncyCastel (PGP). EOFException. No clue what went wrong. The mail was not stored in the db correctly.");
			return "";
		} catch (Exception e) {
			// TODO: Need to fix this!
			logger.warning("Error in BouncyCastel (PGP). EOFException. No clue what went wrong. The mail was not stored in the db correctly.");
			return "";
		}finally {
			in.close();
		}
		return ERROR;
	}

	//Get Password an decrypt Mailsubject:
	public String decryptSubject(String uri) {
		String result = db.getSubjectWhereMailId(uri);
		if (result == null)
			return ERROR;
		else
			return result;
	}
		
	//Find Key and decrypt Mailsubject:
	private String decryptSubject(String encHdr, char[] passwd){
		if (!encHdr.startsWith("-----SUBJECT ENCRYPTED-----") || encHdr.length() < 28)
			return "";
		try {
			InputStream in = new ByteArrayInputStream(Base64.decode(encHdr.substring(27).getBytes()));
			return decryptMail(in, passwd, "UTF-8");
		} catch (NoSuchProviderException e) {
			e.printStackTrace();
		} catch (IOException e) {
			e.printStackTrace();
		}
		return "";
	}
	
	/**
	 * Parse an as multipart detected mail.
	 * 
	 * @param mail
	 *            the mail as {@link String}
	 * @return the parsed mail as {@link String}
	 */
	private String parseMultipart(String mail) {
		StringBuffer result = new StringBuffer();
		String boundaryIndicator = "boundary=";
		String contentPlain = "Content-Type: text/plain;";
		String contentHTML = "Content-Type: text/html;";
		String contentMultipart = "Content-Type: multipart";
		int startIndex = mail.indexOf(boundaryIndicator)+boundaryIndicator.length()+1;
		int endIndex = mail.indexOf("\"", startIndex);
		if (endIndex > 0){
			String boundaryString = Pattern.quote((String) mail.subSequence(startIndex, endIndex));
			String[] parts = mail.split(boundaryString);
			for (String s : parts) {
				if (s.contains(contentMultipart)){
					result.append(parseMultipart(s));
				} else if (s.contains(contentPlain) || s.contains(contentHTML)) {
					String encoding;
					if (s.toLowerCase().contains("charset"))
						encoding = getCharset(s);
					else
						encoding = "ISO-8859-1";
					String sh = s.replaceAll("Content-(Type|ID|Disposition|Transfer-Encoding):.*", "");
					sh = sh.replaceAll("--", "");
					sh = sh.replaceAll("=([A-Fa-f0-9]{2})", "%$1"); // XXX: what if there is = in the mail?! // XXX: is this correct for every encoding?
					try {
						sh = URLDecoder.decode(sh, encoding);
					} catch (UnsupportedEncodingException e) {
						e.printStackTrace();
					}
					result.append(sh.trim()+"\n");
				}
			}
			return result.toString();
		} else {
			return "";
		}
	}

	/**
	 * Search a secret key ring collection for a secret key corresponding to
	 * keyID if it exists.
	 * 
	 * @param pgpSec
	 *            a secret key ring collection.
	 * @param keyID
	 *            keyID we want.
	 * @param pass
	 *            passphrase to decrypt secret key with.
	 * @return the private key as {@link PGPPrivateKey} if found, else NULL
	 * @throws PGPException
	 * @throws NoSuchProviderException
	 */
	private PGPPrivateKey findSecretKey(PGPSecretKeyRingCollection pgpSec, long keyID, char[] pass) throws PGPException, NoSuchProviderException {
		PGPSecretKey pgpSecKey = pgpSec.getSecretKey(keyID);

		if (pgpSecKey == null) {
			return null;
		}

		PGPPrivateKey pKey = null;
		try {
			pKey = pgpSecKey.extractPrivateKey(pass, "BC");
		} catch (PGPException e) {
			if (e.toString().equals("org.bouncycastle.openpgp.PGPException: checksum mismatch at 0 of 20")){
				this.identities = getIDs(pgpSec, keyID);
			}
		}
		return pKey;
	}
	
	/**
	 * Get the identities of a keyID from a Secret key ring collection
	 * 
	 * @param pgpSec
	 *            the {@link PGPSecretKeyRingCollection}
	 * @param keyID
	 *            the ID to search for as long
	 * @return an String with all found identities, seperated in lines
	 */
	@SuppressWarnings("unchecked")
	private String getIDs(PGPSecretKeyRingCollection pgpSec, long keyID){
		PGPSecretKeyRing pgpSecKeyRing;
		StringBuffer sb = new StringBuffer();
		try {
			pgpSecKeyRing = pgpSec.getSecretKeyRing(keyID);
			Iterator<String> id = pgpSecKeyRing.getPublicKey().getUserIDs();
			sb.append("<HTML><BODY>");
			while (id.hasNext()){
				sb.append(id.next().replaceAll("<", "").replaceAll(">", ""));
				sb.append("<BR/>");
			}
			sb.append("</HTML></BODY>");
		} catch (PGPException e) {
			logger.warning("Error while trying to get the id's of the key. "+  e.getLocalizedMessage());
			throw new NullPointerException("Error while trying to get the id's of the key.");
		}
		return sb.toString();
	}
	
	/**
	 * 
	 * Class to prompt for a (pgp) password and save it.
	 * 
	 * @author Franziskus Kiefer
	 *
	 */
	private class PwdPrompt extends JFrame implements ActionListener {
		
		private static final long serialVersionUID = -7006026558608593047L;
		JButton sendButton;
		JButton cancelButton;
		JPanel panel;
		JLabel label;
		JLabel header = new JLabel("<HTML><BODY><H3>Please enter the password for the PGP-Key with the following ID's.</H3></BODY></HTML>");
		final JPasswordField text;
		private boolean abort = false;

		/**
		 * Constructor to get a password prompt.
		 * 
		 * @param folder
		 *            the {@link String} to display that the user knows which
		 *            password to enter.
		 */
		protected PwdPrompt(String folder) {
			label = new JLabel();
			label.setText(folder);
			label.setDropTarget(null); //to fix Linux-Java-Bug
			text = new JPasswordField(15);
			text.setDropTarget(null);//to fix Linux-Java-Bug
			text.addActionListener(this);

			sendButton = new JButton("OK");
			cancelButton = new JButton("Cancel");

			panel = new JPanel();
			panel.setLayout(new BoxLayout(panel, BoxLayout.PAGE_AXIS));
			JPanel toppanel = new JPanel();
			toppanel.add(header, BorderLayout.PAGE_START);
			toppanel.add(label, BorderLayout.LINE_START);
			panel.add(toppanel, BorderLayout.PAGE_START);
			text.setBorder(new EmptyBorder(10, 10, 10, 10));
			JPanel centerPanel = new JPanel();
			centerPanel.add(text, BorderLayout.CENTER);
			panel.add(text, BorderLayout.CENTER);
			JPanel bottompanel = new JPanel();
			bottompanel.add(sendButton, BorderLayout.LINE_START);
			bottompanel.add(cancelButton, BorderLayout.LINE_START);
			panel.add(bottompanel, BorderLayout.PAGE_END);

			pack();
			add(panel, BorderLayout.CENTER);
			sendButton.addActionListener(this);
			ActionListener a1 = new ActionListener() {
			      public void actionPerformed( ActionEvent e ) {
			        abort = true;
			        snotify();
			      }
			};
			cancelButton.addActionListener(a1);
						
			setTitle("PGP Password Request");
			pack();
			
			this.addWindowListener(new java.awt.event.WindowAdapter() {

				public void windowClosing(WindowEvent winEvt) {
					abort = true;
					snotify();
				}
			});
		}

		/**
		 * Add the pwd to the password list.
		 * 
		 * @param ae
		 *            the {@link ActionEvent}
		 */
		@Override
		public void actionPerformed(ActionEvent ae) {
			String pwd = new String(text.getPassword());
			if (!pwd.trim().isEmpty())
				passwds.add(pwd);
			text.setText("");
			snotify();
		}
		
		private void snotify(){
			dispose();
			synchronized (this) {
				notify();
			}
		}
		
	}
	
	/**
	 * Utility function to print the whole {@link Database}
	 */
	protected void viewDB(){
		synchronized (db) {
			db.viewDB();
		}
	}
	
	/**
	 * Drop DB (table) on uninstall.
	 */
	public void deleteDB(){
		synchronized (db) {
			db.clear();
		}
	}

	/**
	 * Remove the entire database from the hard disk. Not only dropping the
	 * table as in {@link #deleteDB()}.
	 */
	public void removeDB() {
		logger.info("Deleting DB...");
		synchronized (db) {
			db.shutDown();
		}
		String path = db.getDbPath();
		File f = new File(path);
		boolean deleted = deleteDir(f);
		if (deleted)
			logger.info("The DB has been successfully deleted.");
		else
			logger.warning("Something went wrong. The DB has not been deleted!");
	}
	
	/**
	 * Deletes all files and subdirectories under dir. Returns true if all
	 * deletions were successful. If a deletion fails, the method stops
	 * attempting to delete and returns false.
	 * 
	 * @param dir
	 *            the Path that should be deleted as {@link File}
	 * @return true if everything went well, else false
	 */
	private boolean deleteDir(File dir) {
		if (dir.isDirectory()) {
			String[] children = dir.list();
			for (int i=0; i<children.length; i++) {
				File toDel = new File(dir, children[i]);
				boolean success = deleteDir(toDel);
				if (!success) {
					logger.warning("Could not delete "+toDel.getAbsolutePath());
					return false;
				}
			}
		}

	    // The directory is now empty so delete it
	    return dir.delete();
	}

	/**
	 * terminate Java for uninstall..
	 */
	public void stopJava(){
		logger.info("terminate java");
		System.exit(0);
	}
	
	/**
	 * Try to detect the correct character encoding of the given String and
	 * correct it if necessary.
	 * 
	 * @param s
	 *            the {@link String} to check
	 * @return the hopefully corrected {@link String}
	 */
	private String getCorrectEncoding(String s) {
		if (containsUtf8Chars(s))
			try {
				return new String(s.getBytes("ISO-8859-1"), "UTF-8");
//				return new String(s.getBytes("UTF-8"), "ISO-8859-1");
			} catch (UnsupportedEncodingException e) {
				e.printStackTrace();
				logger.severe("Error while trying to encode String. "+ e.getLocalizedMessage());
				return s;
			}
		return s;
	}

	/**
	 * Utility function to check whether a string contains specific characters
	 * or not.
	 * 
	 * @param s
	 *            the {@link String} to test
	 * @return true if it contains the specific characters, else false
	 */
	private boolean containsUtf8Chars(String s) {
		for (String c : utf8Chars)
			if (s.contains(c))
				return true;
		return false;
	}
	
	public String encryptSubject(String[] userIDs, String subject, String pubringPath, String pubringFile){
		SubjectEncryption se = new SubjectEncryption();
		return se.encryptSubject(userIDs, subject, pubringPath, pubringFile);
	}
	
	public String getMessageBodyByURI(String uri){
		return db.getBodyWhereMailId(uri);
	}
	
	// TODO: Calender decryption on the fly / encrypt things
	public String encryptContent(String[] userIDs, String subject, String pubringPath, String pubringFile){
		ContentEncryption ce = new ContentEncryption();
		return ce.encryptContent(userIDs, subject, pubringPath, pubringFile);
	}
	
	public String decryptCalendarContent(String in, String passwd, String charset, String pgp_path, String gpg_secret_keyring) throws NoSuchProviderException, IOException{
		CalendarDecryption cd = new CalendarDecryption();
		return cd.decryptCalendarContent(in, passwd, charset, pgp_path, gpg_secret_keyring);
	}
	
	public String decryptCalendarTitel(String encHdr, String passwd, String charset, String pgp_path, String gpg_secret_keyring){
		CalendarDecryption ct = new CalendarDecryption();
		return ct.decryptCalendarTitel(encHdr, passwd, charset, pgp_path, gpg_secret_keyring);
	}
	
	public String getMessageSubjectByURI(String uri){
		return db.getSubjectWhereMailId(uri);
	}
	
}
